/*
 * Copyright (C) 2013 Daniel Wasilew <daniel.wasilew@codeminer.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.codeminer.parts.settings;

import org.codeminer.parts.application.ApplicationComponent;
import org.codeminer.parts.application.ApplicationContext;
import org.codeminer.parts.application.ShutdownCallback;
import org.codeminer.parts.files.FileException;
import org.codeminer.parts.files.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author Daniel Wasilew <daniel.wasilew@codeminer.org>
 */
public final class Settings implements ApplicationComponent {

    private static final Logger log = LoggerFactory.getLogger(Settings.class);
    private static final String CURRENT_SQL_LOCATION = "sql.current.location";
    private static final String DISCOVERY_MAX_THREADS = "discovery.threads.max";
    private static final String DISCOVERY_PORT = "discovery.port";
    private static final String SQL_UPGRADE_SCRIPT_LOCATION = "sql.update.location";
    private final List<SettingsListener> listeners = new LinkedList<>();
    private ScheduledExecutorService periodicChecker = Executors.newSingleThreadScheduledExecutor();
    private Properties properties = new Properties();
    private String customFileHash = null;
    private File settingsFile;

    public Settings() {
        initDefaultSettings();
    }

    private void startWatchThread(final File settingsFile) {
        long settingsUpdateInterval = getSettingsUpdateInterval();
        log.debug("will schedule settings check interval @[{}]ms", settingsUpdateInterval);
        periodicChecker.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                updateSettings(settingsFile);

            }
        }, 0, settingsUpdateInterval, TimeUnit.MILLISECONDS);
    }

    void updateSettings(File settingsFile) {
        if (settingsFile.exists()) {
            try {
                String currentHash = FileUtil.calculateMD5Hash(settingsFile);
                if (!currentHash.equals(this.customFileHash)) {
                    log.trace("custom properties file changed, will now reload");
                    initDefaultSettings();
                    readSettingsFile(settingsFile);
                    fireChange();
                }
            } catch (FileException e) {
                log.debug("could not calculate md5 hash", e);
            }
        } else {
            if (customFileHash != null) {
                customFileHash = null;
                initDefaultSettings();
                fireChange();
            }
        }
    }

    private void fireChange() {
        for (SettingsListener listener : listeners) {
            listener.changed();
        }
        stopAndRestartUpdateChecker();
    }

    private void stopAndRestartUpdateChecker() {
        this.periodicChecker.shutdown();
        try {
            this.periodicChecker.awaitTermination(1000, TimeUnit.MILLISECONDS);
            this.periodicChecker = Executors.newSingleThreadScheduledExecutor();
            startWatchThread(settingsFile);
        } catch (InterruptedException e) {
            log.trace("awaiting termination runs into:", e);
        }
    }

    public void addListener(SettingsListener listener) {
        this.listeners.add(listener);
    }

    @Override
    public void shutdown() {
        this.periodicChecker.shutdown();
        try {
            this.periodicChecker.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("could not shutdown periodic checker", e);
        }
    }

    @Override
    public void startup(ApplicationContext context, ShutdownCallback shutdownCallback) throws Exception {
        initDefaultSettings();
        settingsFile = new File(context.getConfigurationDir(), "custom.properties");
        readSettingsFile(settingsFile);
        startWatchThread(settingsFile);
    }

    public long getSettingsUpdateInterval() {
        return Long.parseLong(properties.getProperty("settings.checkInterval"));
    }

    private URL getURL(String url) {
        URL result = null;
        try {
            result = new URL(url);
        } catch (MalformedURLException ex) {
            log.error("could not parse url from [{}]", url, ex);
        }
        return result;
    }

    private void initDefaultSettings() {
        InputStream is = null;
        try {
            is = Settings.class.getResourceAsStream("/settings.properties");
            if (is != null) {
                properties.load(is);
            }
        } catch (IOException ex) {
            log.error("could not read default settings", ex);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ex) {
                    log.trace("could not close stream", ex);
                }
            }
        }
    }

    private void readSettingsFile(File settingsFile) {
        if (!settingsFile.exists()) {
            log.trace("custom settings file [{}] doesn't exists", settingsFile.getAbsolutePath());
            return;
        }
        try {
            this.customFileHash = FileUtil.calculateMD5Hash(settingsFile);
        } catch (FileException e) {
            log.debug("could not calculate md5 hash", e);
            this.customFileHash = null;
        }
        Properties tmpProperties = new Properties(properties);
        InputStream fis = null;
        try {
            fis = new FileInputStream(settingsFile);
            tmpProperties.load(fis);
            this.properties = tmpProperties;
            log.info("current settings are:[{}]", properties.toString());
        } catch (IOException ex) {
            log.error("could not read custom properties file", ex);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ex) {
                    log.trace("could not close stream", ex);
                }
            }
        }
    }

    String getUpdateVersionInfoLocation() {
        return this.properties.getProperty("updateVersionInfoLocation");
    }

    public URL getUpdateVersionInfoURL() {
        String url = getUpdateBaseLocation() + getUpdateVersionInfoLocation();
        return getURL(url);
    }

    public URL getUpdatePackageURL() {
        String url = getUpdateBaseLocation() + getUpdatePackageLocation();
        return getURL(url);
    }

    public String getUpdatePackageLocation() {
        return this.properties.getProperty("updatePackageLocation");
    }

    public String getUpdateBaseLocation() {
        return this.properties.getProperty("updateBaseLocation");
    }

    public long getUpdateCheckInterval() {
        return Long.parseLong(properties.getProperty("updateCheckInterval"));
    }

    public int getWebAccessPort() {
        return Integer.parseInt(properties.getProperty("entry.webaccess.port"));
    }

    public long getTimout() {
        return Long.parseLong(properties.getProperty("timeout"));
    }

    public void setTimeout(long timeOut) {
        this.properties.setProperty("timeout", timeOut + "");
    }

    public int getSocketEntrancePort() {
        return Integer.parseInt(this.properties.getProperty("entry.socket.port"));
    }

    public int getMaxSocketConnections() {
        return Integer.parseInt(this.properties.getProperty("entry.socket.maxConnections"));
    }

    public boolean isUpdateEnabled() {
        return Boolean.valueOf(this.properties.getProperty("enableUdpateCheck"));
    }

    public int getMaxSocketActionThreads() {
        return Integer.valueOf(this.properties.getProperty("session.maxThreads"));
    }

    public String getCurrentSQLScriptLocation() {
        return this.properties.getProperty(CURRENT_SQL_LOCATION);
    }

    public void setCurrentSQLScriptLocation(String newSqlInitScriptLocation) {
        this.properties.setProperty(CURRENT_SQL_LOCATION, newSqlInitScriptLocation);
    }

    public String getSQLUpdateScriptLocations() {
        return this.properties.getProperty(SQL_UPGRADE_SCRIPT_LOCATION);
    }

    @Override
    public Class getDescriptionClass() {
        return Settings.class;
    }

    public int getDiscoveryMaxThreads() {
        return Integer.parseInt(properties.getProperty(DISCOVERY_MAX_THREADS));
    }

    public int getDiscoveryPort() {
        return Integer.parseInt(properties.getProperty(DISCOVERY_PORT));
    }
}
